package cn.sinohealth;

import cn.sinohealth.model.EsConstantModel;
import cn.sinohealth.model.PropertyType;
import cn.sinohealth.util.TemplateUtils;
import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.serializer.SerializerFeature;

import javax.annotation.processing.*;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.*;
import javax.lang.model.type.DeclaredType;
import javax.lang.model.util.Elements;
import javax.tools.Diagnostic;
import javax.tools.FileObject;
import javax.tools.JavaFileObject;
import javax.tools.StandardLocation;
import java.io.IOException;
import java.io.Writer;
import java.util.*;
import java.util.stream.Collectors;

import static cn.sinohealth.model.PropertyType.*;

/**
 * @author qinminghui
 * @description TODO
 * @date 2020/9/17
 */
public class EsMapperProcessor extends AbstractProcessor {
    public static final String ES_FIELD_REFERENCE = EsField.class.getCanonicalName();
    public static final String NESTED_OBJECT = "nestedObject";
    private Filer filer;
    private Messager messager;
    private Elements elementUtils;

    private final Set<MappingFile> mappingFiles = new HashSet<>();
    private final Set<ConstantFile> constantFiles = new HashSet<>();


    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);
        filer = processingEnv.getFiler();
        messager = processingEnv.getMessager();
        elementUtils = processingEnv.getElementUtils();
    }

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        if (annotations == null || annotations.size() == 0) {
            return false;
        }

        if (annotations.size() != 1 || !EsMapper.class.getCanonicalName().equals(annotations.iterator().next().asType().toString())) {
            return false;
        }

        processEsMapper(roundEnv);
        writeFile();


        return false;
    }

    private void processEsMapper(RoundEnvironment roundEnv) {
        Set<? extends Element> esMapperElements = roundEnv.getElementsAnnotatedWith(EsMapper.class);
        if (esMapperElements == null || esMapperElements.size() < 1) {
            return;
        }

        for (Element e : esMapperElements) {
            EsMapper esMapperAnnotation = e.getAnnotation(EsMapper.class);
            String classAlias = esMapperAnnotation.value();
            String indicesAlias = esMapperAnnotation.alias();
            String mapping = esMapperAnnotation.mapping();
            boolean allKeywordUseNormalizer = esMapperAnnotation.allKeywordUseNormalizer();
            String normalizerName = esMapperAnnotation.normalizer();
            boolean enabled = esMapperAnnotation.enabled();

            String className = e.getSimpleName().toString();

            List<Element> enclosedElements = getElements((DeclaredType) e.asType());
            enclosedElements = enclosedElements.stream().collect(
                    Collectors.collectingAndThen(
                            Collectors.toCollection(() -> new TreeSet<>(Comparator.comparing(o -> o.getSimpleName().toString())))
                            , ArrayList::new));

            String pkName = getPkgName(e);
            note(String.format("package = %s", pkName));
            final String constantClassName = getConstantClassName(className, classAlias);

            constantFiles.add(new ConstantFile(pkName, constantClassName, indicesAlias, enclosedElements, String.format("mappings/%s-mappings.json", mapping)));
            mappingFiles.add(new MappingFile(mapping, enclosedElements, normalizerName, allKeywordUseNormalizer,enabled));

            //4.生成文件
//            createConstantFile(pkName, constantClassName, indicesAlias, enclosedElements);
//
//            if (!isEmpty(mapping)) {
//                createMappingsJson(mapping, enclosedElements);
//            }
        }

    }

    void writeFile() {
        constantFiles.forEach(cf -> {
            createConstantFile(cf.getPkName(), cf.getConstantClassName(), cf.getIndicesAlias(), cf.getEnclosedElements(), cf.getMappingFile());
        });

        mappingFiles.forEach(mf -> {
            String mapping = mf.getMapping();
            if (!isBlank(mapping)) {
                createMappingsJson(mf);
            }
        });
    }

    class MappingFile {
        private String mapping;
        private List<Element> enclosedElements;
        private String normalizerName;
        private boolean allKeywordUseNormalizer;
        private boolean enabled;


        public MappingFile(String mapping, List<Element> enclosedElements, String normalizerName, boolean allKeywordUseNormalizer, boolean enabled) {
            this.mapping = mapping;
            this.enclosedElements = enclosedElements;
            this.normalizerName = normalizerName;
            this.allKeywordUseNormalizer = allKeywordUseNormalizer;
            this.enabled = enabled;
        }

        public String getMapping() {
            return mapping;
        }

        public List<Element> getEnclosedElements() {
            return enclosedElements;
        }

        public String getNormalizerName() {
            return normalizerName;
        }

        public boolean isAllKeywordUseNormalizer() {
            return allKeywordUseNormalizer;
        }
    }

    class ConstantFile {
        private String pkName;
        private String constantClassName;
        private String indicesAlias;
        private final String mappingFile;
        private List<Element> enclosedElements;

        public ConstantFile(String pkName, String constantClassName, String indicesAlias, List<Element> enclosedElements, String mappingFile) {
            this.constantClassName = constantClassName;
            this.pkName = pkName;
            this.enclosedElements = enclosedElements;
            this.indicesAlias = indicesAlias;
            this.mappingFile = mappingFile;
        }

        public String getPkName() {
            return pkName;
        }

        public String getIndicesAlias() {
            return indicesAlias;
        }

        public List<Element> getEnclosedElements() {
            return enclosedElements;
        }

        public String getConstantClassName() {
            return constantClassName;
        }

        public String getMappingFile() {
            return mappingFile;
        }
    }

    public List<Element> getElements(DeclaredType e) {
        TypeElement typeElement = (TypeElement) e.asElement();
        List<Element> enclosedElements = (List<Element>) typeElement.getEnclosedElements();
        final List<Element> subElements = new ArrayList<>(enclosedElements);

        DeclaredType superclass = (DeclaredType) typeElement.getSuperclass();
        Name simpleName = superclass.asElement().getSimpleName();
        if (simpleName.contentEquals("Object")) {
            return subElements;
        }
        List<Element> superElementList = getElements(superclass);
        subElements.addAll(superElementList);
        return subElements;
    }

    /**
     * @param className
     * @param classAlias
     * @return
     */
    private String getConstantClassName(String className, String classAlias) {
        if (!isBlank(classAlias)) {
            String startValue = classAlias.substring(0, 1);
            String endValue = classAlias.substring(1);
            className = startValue.toUpperCase() + endValue;
        }
        return className + "Constant";
    }

    private boolean isBlank(String str) {
        return str == null || str.trim().length() == 0;
    }

    private String getPkgName(Element e) {
        PackageElement packageElement = elementUtils.getPackageOf(e);
        String pkName = packageElement.getQualifiedName().toString();
        return pkName;
    }

    @Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.latestSupported();
    }

    private void createConstantFile(String pkName, String className, String aliasIndices, List<? extends Element> enclosedElements, String mappingFile) {
        //排除常量属性字段
        List<String> fieldList = new ArrayList<>();
        enclosedElements.stream().filter(
                v -> (v instanceof VariableElement) && ((VariableElement) v).getConstantValue() == null
        ).forEach(v ->
                {
                    List<String> propertyNameList = getConstantPropertyPath("", (VariableElement) v);
                    fieldList.addAll(propertyNameList);
                }
        );


        String canonicalClassName = pkName + "." + className;

        Writer writer = null;
        try {
            JavaFileObject jfo = filer.createSourceFile(canonicalClassName, new Element[]{});
            writer = jfo.openWriter();
            final EsConstantModel esConstantModel = new EsConstantModel();
            esConstantModel.setPkName(pkName);
            esConstantModel.setClassName(className);
            esConstantModel.setFieldList(fieldList);
            esConstantModel.setAliasIndices(aliasIndices);
            esConstantModel.setMappingFile(mappingFile);

            final String esConstantClass = TemplateUtils.writeEsConstant(esConstantModel);
            writer.write(esConstantClass);
            writer.flush();
            writer.close();
            note("generate constant in " + pkName);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (writer != null) {
                try {
                    writer.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }

    }

    public List<String> getConstantPropertyPath(String parentName, VariableElement v) {
        final String fieldName = v.getSimpleName().toString();
        String currentPathName = parentName != null && parentName.trim().length() > 0 ? parentName + "." + fieldName : fieldName;

        List<String> fieldList = new ArrayList<>();
        fieldList.add(currentPathName);

        PropertyType esType = getESType(v);
        if (esType == NESTED || esType == OBJECT) {
            final List<? extends Element> nestedObjectFields = getNestedObjectFields(v.getAnnotationMirrors());
            nestedObjectFields.stream().forEach(n -> {
                List<String> propertyNameList = getConstantPropertyPath(currentPathName, (VariableElement) n);
                fieldList.addAll(propertyNameList);
            });
        }
        return fieldList;

    }

    public void createMappingsJson(MappingFile mappingFile) {
        String mappingFileName = mappingFile.getMapping();
        List<Element> enclosedElements = mappingFile.getEnclosedElements();
        String normalizerName = mappingFile.getNormalizerName();
        boolean allKeywordUseNormalizer = mappingFile.isAllKeywordUseNormalizer();

        final List<? extends Element> varibaleElements = enclosedElements.stream().filter(v -> (v instanceof VariableElement) && ((VariableElement) v).getConstantValue() == null)
                .collect(Collectors.toList());
        JSONObject mappingJson = new JSONObject();
        JSONObject property = new JSONObject();

        if (!mappingFile.enabled) {
            mappingJson.put("enabled", false);
        }
        mappingJson.put("properties", property);

        varibaleElements.stream().forEach(v -> {
            final String propertyName = v.getSimpleName().toString();
            JSONObject propertyValue = generatePropertyValue(v, normalizerName, allKeywordUseNormalizer);
            property.put(propertyName, propertyValue);
        });


        Writer writer = null;
        try {
            FileObject resource = filer.createResource(StandardLocation.CLASS_OUTPUT, "mappings", mappingFileName + "-mappings.json", new Element[]{});
            writer = resource.openWriter();
            final String prettyJSON = mappingJson.toString(SerializerFeature.PrettyFormat);
            writer.write(prettyJSON);
            writer.flush();
            writer.close();
        } catch (IOException e) {
            note(String.format("generate mapping json error:%s", e.getMessage()));
        } finally {
            if (writer != null) {
                try {
                    writer.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    public PropertyType getESType(Element v) {
        final EsField esFieldAnnotation = v.getAnnotation(EsField.class);

        if (esFieldAnnotation == null) {
            return KEYWORD;
        }

        return esFieldAnnotation.type();
    }


    public Boolean getFieldEnabled(Element v) {
        final EsField esFieldAnnotation = v.getAnnotation(EsField.class);

        if (esFieldAnnotation == null) {
            return true;
        }
        return esFieldAnnotation.enabled();
    }

    private JSONObject generatePropertyValue(Element variableElement, String normalizerName, boolean allKeywordUseNormalizer) {
        JSONObject propertyValue = new JSONObject();

        final PropertyType esType = getESType(variableElement);
        final String typeValue = esType.getValue();
        propertyValue.put("type", typeValue);

        Boolean fieldEnabled = getFieldEnabled(variableElement);
        if (esType == OBJECT && !fieldEnabled) {
            propertyValue.put("enabled", false);
        }

        final EsField esField = variableElement.getAnnotation(EsField.class);
        if (Objects.nonNull(esField)){
            //copy_to handle
            String[] copyTo = esField.copyTo();
            if (copyTo.length > 0) {
                propertyValue.put("copy_to", new HashSet<>(Arrays.asList(copyTo)));
            }
        }

        switch (esType) {
            case KEYWORD:
                if (allKeywordUseNormalizer && !isBlank(normalizerName)) {
                    propertyValue.put("normalizer", normalizerName);
                }
            case TEXT:
                break;
            case DATE:
                final EsField esFieldAnnotation = variableElement.getAnnotation(EsField.class);
                String format = esFieldAnnotation.format();
                propertyValue.put("format", format);
                break;
            case NESTED:
            case OBJECT:
                final JSONObject nestedProperty = new JSONObject();
                propertyValue.put("properties", nestedProperty);

                final List<? extends AnnotationMirror> annotationMirrors = variableElement.getAnnotationMirrors();
                if (annotationMirrors == null || annotationMirrors.size() == 0) {
                    return nestedProperty;
                }


                List<? extends Element> nestedObjectFields = getNestedObjectFields(annotationMirrors);
                nestedObjectFields.stream().forEach(f -> {
                    final String name = f.getSimpleName().toString();
                    final JSONObject nestedJsonObject = generatePropertyValue(f, normalizerName, allKeywordUseNormalizer);
                    nestedProperty.put(name, nestedJsonObject);
                });
                break;
            default:
                break;
        }

        return propertyValue;
    }


    /**
     * 获取nested object里面的属性
     *
     * @param
     * @return
     */
    public List<? extends Element> getNestedObjectFields(List<? extends AnnotationMirror> annotationMirrors) {
        if (annotationMirrors == null || annotationMirrors.size() == 0) {
            return Collections.emptyList();
        }

        final AnnotationMirror annotationMirror = annotationMirrors.stream().filter(a -> a.getAnnotationType().toString().equals(ES_FIELD_REFERENCE)).collect(Collectors.toList()).get(0);
        final Optional<? extends AnnotationValue> nestedObjectOptional = annotationMirror.getElementValues().entrySet().stream().filter(e -> NESTED_OBJECT.equals(e.getKey().getSimpleName().toString())).map(e -> e.getValue()).findAny();
        if (!nestedObjectOptional.isPresent()) {
            return Collections.emptyList();
        }

        final DeclaredType value = (DeclaredType) nestedObjectOptional.get().getValue();
//        final List<? extends Element> enclosedElements = value.asElement().getEnclosedElements();
        List<Element> enclosedElements = getElements(value);
        if (enclosedElements == null || enclosedElements.size() == 0) {
            return Collections.emptyList();
        }

        return enclosedElements.stream().filter(f -> (f instanceof VariableElement) && ((VariableElement) f).getConstantValue() == null)
                .collect(Collectors.toList());
    }

    @Override
    public Set<String> getSupportedAnnotationTypes() {
        final Set<String> set = new HashSet<>();
        set.add(EsMapper.class.getCanonicalName());
        return set;
    }

    private void note(String msg) {
        messager.printMessage(Diagnostic.Kind.NOTE, msg);
    }

    private void error(String msg) {
        messager.printMessage(Diagnostic.Kind.ERROR, msg);
    }
}
